JDBC try/catch/finally 코드의 문제점

현재의 DAO는 복잡한 try/catch/finally 블록이 2종으로 중첩되고, 모든 메소드마다 반복되고 있다. 이런 코드를 효과적으로 다루기 위한 핵심은 변하지 않는, 그러나 많은 곳에서 중복되는 코드와 로직에 따라 자꾸 확장되고 자꾸 변하는 코드를 잘 분리해내는 작업이다.

분리와 재사용을 위한 디자인 패턴 적용

메소드 추출

먼저 생각해볼 수 있는 방법은 변하는 부분을 메소드로 빼는 것이다. 하지만 보통 메소드 추출 리팩토링을 적용하는 경우에는 분리시킨 메소드를 다른 곳에서 재사용할 수 있어야 하는데, 분리된 메소드가 로직마다 새롭게 만들어서 확장돼야 하는 부분이라면 뭔가 반대로 했다.

템플릿 메소드 패턴의 적용

템플릿 메소드 패턴은 상속을 통해 기능을 확장해서 사용하는 부분이다. 변하지 않는 부분은 슈퍼클래스에 두고 변하는 부분은 추상 메소드로 정의해둬서 서브클래스에서 오버라이드하여 새롭게 정희해 쓰도록 하는 것이다.

하지만 로직마다 상속을 통해 새로운 클래스를 만들어야 한다는 문제가 있다. 이는 확장 구조가 이미 클래스를 설계하는 시점에 고정이 되어 버려서, 관계에 대한 유연성이 떨어진다.

전량 패턴의 적용

OCP(개방 폐쇄 원칙)을 잘 지키는 구조이면서도 템프릸 메소드 패턴보다 유연하고 확장성이 뛰어난 것이, 오브젝트를 아예 둘로 분리하고 클래스 레벨에서는 인터페이스를 통해서만 의존하도록 만드는 전략 패턴이다. 전략 패턴은 OCP 관점에 보면 화장에 해당하는 변하는 부분을 별도의 클래스로 만들어 추상화된 인터페이스를 통해 위임하는 방식이다. Context의 contextMethod()에서 일정한 구조를 가지고 동작하다가 특정 확장 기능은 Strategy 인터페이스를 통해 외부의 독립된 전략 클래스에 위임하는 것이다.

deleteAll()은 JDBC를 이용해 DB를 업데이트하는 작업이라는 변하지 않는 맥락(context)을 갖는다. 다음과 같다 - DB 커넥션 가져오기 - PreparedStatement를 만들어줄 외부 기능 호출하기 - 전달받은 PreparedStatement 실행하기 - 예외가 발생하면 이를 다시 메소드 밖으로 던지기 - 모든 경우에 만들어진 PreparedStatementConnection을 적절히 닫아주기

이때, PreparedStatement를 만들어주는 외부 기능이 바로 전략 패턴에서 말하는 전략이라고 볼 수 있다.

public void deleteAll() throws SQLException {
    ...
    try{
        c = dataSource.getConnection();

        StatementStrategy strategy = new DeleteAllStatement();
        ps = strategy.makePreparedStatement(c);

        ps.executeUpdate();
    } catch(SQLException){ 
    ...
}

하지만 이렇게 컨텍스트 안에서 이미 구체적인 전략 클래스인 DeleteAllStatement를 사용하도록 고정되어 있다면 뭔가 이상하다. 컨텍스트가 StatementStrategy 인터페이스뿐 아니라 특정 구현 클래스인 DeleteAllStatement를 직접 알고 있다는 건, 전략 패턴에도 OCP에도 잘 들어맞는다고 볼 수 없기 때문이다.

### DI 적용을 위한 클라이언트/컨텍스트 분리

전략 패턴에 따르면 Context가 어떤 전략을 사용하게 할 것인가는 Context를 사용하는 앞단의 Client가 결정하는 게 이란적이다. Client가 구체적인 전략의 하나를 선택하고 오브젝트로 만들어서 Context에 전달하는 것이다. Context는 전달받은 그 Strategy 구현 클래스의 오브젝트를 사용한다. 이는 일반화한 것이 DI이며, 결국 DI란 이러한 전략 패턴의 장점을 일반적으로 활용할 수 있도록 만든 구조라고 볼 수 있다.

클라이언트로부터 StatementStrategy타입의 전략 오브젝트를 제공받고 JDBC try/catch/finally 구조로 만들어진 컨텍스트 내에서 작업을 수행한다.

// 메소드로 분리한 try/catch/finally 컨텍스트 코드 
public void jdbcContextWithStatementStrategy(StatementStrategy stmt) throws SQLException {
        Connection c = null;
        PreparedStatement ps = null;

        try{
            c = dataSource.getConnection();

            ps = stmt.makePreparedStatement(c);

            ps.executeUpdate();
        } catch (SQLException e ){
            throw e;
        } finally {
            if(ps != null) { try { ps.close(); } catch (SQLException e){} }
            if(c != null) { try { c.close(); } catch (SQLException e){} }
        }
}

컨텍스트를 별도의 메소드로 분리했으니 deleteAll() 메소드가 클라이언트가 된다. deleteAll()은 전략 오브젝트를 만들고 컨텍스트를 호출하는 책임을 지고 있다.

// 클라이언트 책임을 담당할 deleteAll()메소드 
public void deleteAll() throws SQLException {
        StatementStrategy strategy = new DeleteAllStatement();
        jdbcContextWithStatementStrategy(strategy);
}